package org.akanework.gramophone.ui.components

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Typeface
import android.os.Build
import android.text.Layout
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.media3.common.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.animation.PathInterpolator
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.TypefaceCompat
import androidx.core.text.getSpans
import androidx.preference.PreferenceManager
import org.akanework.gramophone.R
import org.akanework.gramophone.logic.dpToPx
import org.akanework.gramophone.logic.getBooleanStrict
import org.akanework.gramophone.logic.ui.spans.MyForegroundColorSpan
import org.akanework.gramophone.logic.ui.spans.MyGradientSpan
import org.akanework.gramophone.logic.ui.spans.StaticLayoutBuilderCompat
import org.akanework.gramophone.logic.utils.CalculationUtils.lerp
import org.akanework.gramophone.logic.utils.CalculationUtils.lerpInv
import org.akanework.gramophone.logic.utils.SemanticLyrics
import org.akanework.gramophone.logic.utils.SpeakerEntity
import org.akanework.gramophone.logic.utils.findBidirectionalBarriers
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.properties.Delegates

private const val TAG = "NewLyricsView"

class NewLyricsView(context: Context, attrs: AttributeSet?) : ScrollingView2(context, attrs),
    GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
    private val smallSizeFactor = 0.97f
    private var lyricAnimTime by Delegates.notNull<Float>()

    private val scaleInAnimTime
        get() = lyricAnimTime / 2f
    private val isElegantTextHeight = false // TODO this was causing issues, but target 36 can't turn this off anymore... needs rework
    private val scaleColorInterpolator = PathInterpolator(0.4f, 0.2f, 0f, 1f)
    private val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
    private lateinit var typeface: Typeface
    private val grdWidth = context.resources.getDimension(R.dimen.lyric_gradient_size)
    private val defaultTextSize = context.resources.getDimension(R.dimen.lyric_text_size)
    private val translationTextSize = context.resources.getDimension(R.dimen.lyric_tl_text_size)
    private val translationBackgroundTextSize =
        context.resources.getDimension(R.dimen.lyric_tl_bg_text_size)
    private val globalPaddingHorizontal = 28.5f.dpToPx(context)
    private var colorSpanPool = mutableListOf<MyForegroundColorSpan>()
    private var spForRender: Pair<IntArray, List<SbItem>>? = null
    private var spForMeasure: Pair<IntArray, List<SbItem>>? = null
    private var lyrics: SemanticLyrics? = null
    private var posForRender = 0uL
    lateinit var instance: Callbacks
    private val gestureDetector = GestureDetector(context, this)
    private var currentScrollTarget: Int? = null
    private var isCallbackQueued = false
    private val invalidateCallback = Runnable { isCallbackQueued = false; invalidate() }

    // -/M/F/D insanity starts here
    private var defaultTextColor = 0
    private var highlightTextColor = 0
    private var defaultTextColorM = 0
    private var highlightTextColorM = 0
    private var defaultTextColorF = 0
    private var highlightTextColorF = 0
    private var defaultTextColorD = 0
    private var highlightTextColorD = 0
    private val defaultTextPaint = TextPaint().apply {
        color = Color.RED
        isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
        textSize = defaultTextSize
    }
    private val translationTextPaint = TextPaint().apply {
        color = Color.GREEN
        isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
        textSize = translationTextSize
    }
    private val translationBackgroundTextPaint = TextPaint().apply {
        color = Color.BLUE
        isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
        textSize = translationBackgroundTextSize
    }
    private val defaultTextPaintM = lazy {
        TextPaint().apply {
            color = defaultTextColorM
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = defaultTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private val translationTextPaintM = lazy {
        TextPaint().apply {
            color = defaultTextColorM
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = translationTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private val defaultTextPaintF = lazy {
        TextPaint().apply {
            color = defaultTextColorF
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = defaultTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private val translationTextPaintF = lazy {
        TextPaint().apply {
            color = defaultTextColorF
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = translationTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private val defaultTextPaintD = lazy {
        TextPaint().apply {
            color = defaultTextColorD
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = defaultTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private val translationTextPaintD = lazy {
        TextPaint().apply {
            color = defaultTextColorD
            isElegantTextHeight = this@NewLyricsView.isElegantTextHeight
            textSize = translationTextSize; typeface = this@NewLyricsView.typeface
        }
    }
    private var wordActiveSpan = MyForegroundColorSpan(Color.CYAN)
    private var wordActiveSpanM = lazy { MyForegroundColorSpan(highlightTextColorM) }
    private var wordActiveSpanF = lazy { MyForegroundColorSpan(highlightTextColorF) }
    private var wordActiveSpanD = lazy { MyForegroundColorSpan(highlightTextColorD) }
    private var gradientSpanPool = mutableListOf<MyGradientSpan>()
    private var gradientSpanPoolM = lazy { mutableListOf<MyGradientSpan>() }
    private var gradientSpanPoolF = lazy { mutableListOf<MyGradientSpan>() }
    private var gradientSpanPoolD = lazy { mutableListOf<MyGradientSpan>() }
    private fun makeGradientSpan() =
        MyGradientSpan(grdWidth, defaultTextColor, highlightTextColor)
    private fun makeGradientSpanM() =
        MyGradientSpan(grdWidth, defaultTextColorM, highlightTextColorM)
    private fun makeGradientSpanF() =
        MyGradientSpan(grdWidth, defaultTextColorF, highlightTextColorF)
    private fun makeGradientSpanD() =
        MyGradientSpan(grdWidth, defaultTextColorD, highlightTextColorD)

    init {
        applyTypefaces()
        loadLyricAnimTime()
    }

    interface Callbacks {
        fun getCurrentPosition(): ULong
        fun seekTo(position: ULong)
        fun setPlayWhenReady(play: Boolean)
        fun speed(): Float
    }

    fun updateTextColor(
        newColor: Int, newHighlightColor: Int, newColorM: Int,
        newHighlightColorM: Int, newColorF: Int, newHighlightColorF: Int,
        newColorD: Int, newHighlightColorD: Int
    ) {
        var changed = false
        var changedM = false
        var changedF = false
        var changedD = false
        if (defaultTextColor != newColor) {
            defaultTextColor = newColor
            defaultTextPaint.color = defaultTextColor
            translationTextPaint.color = defaultTextColor
            translationBackgroundTextPaint.color = defaultTextColor
            changed = true
        }
        if (defaultTextColorM != newColorM) {
            defaultTextColorM = newColorM
            if (defaultTextPaintM.isInitialized()) {
                defaultTextPaintM.value.color = defaultTextColorM
                changedM = true
            }
            if (translationTextPaintM.isInitialized()) {
                translationTextPaintM.value.color = defaultTextColorM
                changedM = true
            }
        }
        if (defaultTextColorF != newColorF) {
            defaultTextColorF = newColorF
            if (defaultTextPaintF.isInitialized()) {
                defaultTextPaintF.value.color = defaultTextColorF
                changedF = true
            }
            if (translationTextPaintF.isInitialized()) {
                translationTextPaintF.value.color = defaultTextColorF
                changedF = true
            }
        }
        if (defaultTextColorD != newColorD) {
            defaultTextColorD = newColorD
            if (defaultTextPaintD.isInitialized()) {
                defaultTextPaintD.value.color = defaultTextColorD
                changedD = true
            }
            if (translationTextPaintD.isInitialized()) {
                translationTextPaintD.value.color = defaultTextColorD
                changedD = true
            }
        }
        if (highlightTextColor != newHighlightColor) {
            highlightTextColor = newHighlightColor
            wordActiveSpan.color = highlightTextColor
            changed = true
        }
        if (highlightTextColorM != newHighlightColorM) {
            highlightTextColorM = newHighlightColorM
            if (wordActiveSpanM.isInitialized()) {
                wordActiveSpanM.value.color = highlightTextColorM
                changedM = true
            }
        }
        if (highlightTextColorF != newHighlightColorF) {
            highlightTextColorF = newHighlightColorF
            if (wordActiveSpanF.isInitialized()) {
                wordActiveSpanF.value.color = highlightTextColorF
                changedF = true
            }
        }
        if (highlightTextColorD != newHighlightColorD) {
            highlightTextColorD = newHighlightColorD
            if (wordActiveSpanD.isInitialized()) {
                wordActiveSpanD.value.color = highlightTextColorD
                changedD = true
            }
        }
        if (changed) {
            gradientSpanPool.clear()
            repeat(3) { gradientSpanPool.add(makeGradientSpan()) }
        }
        if (changedM && gradientSpanPoolM.isInitialized()) {
            gradientSpanPoolM.value.clear()
            repeat(3) { gradientSpanPoolM.value.add(makeGradientSpanM()) }
        }
        if (changedF && gradientSpanPoolF.isInitialized()) {
            gradientSpanPoolF.value.clear()
            repeat(3) { gradientSpanPoolF.value.add(makeGradientSpanF()) }
        }
        if (changedD && gradientSpanPoolD.isInitialized()) {
            gradientSpanPoolD.value.clear()
            repeat(3) { gradientSpanPoolD.value.add(makeGradientSpanD()) }
        }
        if (changed || changedM || changedF || changedD) {
            spForRender?.second?.forEach {
                it.text.getSpans<MyGradientSpan>()
                    .forEach { s -> it.text.removeSpan(s) }
            }
            invalidate()
        }
    }

    fun updateLyrics(parsedLyrics: SemanticLyrics?) {
        spForRender = null
        spForMeasure = null
        requestLayout()
        lyrics = parsedLyrics
    }

    fun updateLyricPositionFromPlaybackPos() {
        if (instance.getCurrentPosition() != posForRender) // if not playing, might stay same
            invalidate()
    }

    fun onPrefsChanged(key: String) {
        if (key == "lyric_no_animation") {
            loadLyricAnimTime()
            return
        }
        if (key == "lyric_bold")
            applyTypefaces()
        spForRender = null
        spForMeasure = null
        requestLayout()
    }

    private fun loadLyricAnimTime() {
        lyricAnimTime = if (prefs.getBooleanStrict("lyric_no_animation", false)) 0f else 650f
    }

    private fun applyTypefaces() {
        typeface = if (prefs.getBooleanStrict("lyric_bold", false)) {
            TypefaceCompat.create(context, null, 700, false)
        } else {
            TypefaceCompat.create(context, null, 500, false)
        }
        defaultTextPaint.typeface = typeface
        translationTextPaint.typeface = typeface
        translationBackgroundTextPaint.typeface = typeface
        if (defaultTextPaintM.isInitialized())
            defaultTextPaintM.value.typeface = typeface
        if (translationTextPaintM.isInitialized())
            translationTextPaintM.value.typeface = typeface
        if (defaultTextPaintF.isInitialized())
            defaultTextPaintF.value.typeface = typeface
        if (translationTextPaintF.isInitialized())
            translationTextPaintF.value.typeface = typeface
        if (defaultTextPaintD.isInitialized())
            defaultTextPaintD.value.typeface = typeface
        if (translationTextPaintD.isInitialized())
            translationTextPaintD.value.typeface = typeface
    }

    override fun onDrawForChild(canvas: Canvas) {
        posForRender = instance.getCurrentPosition().also {
            if (posForRender > it && posForRender - it < 1000uL)
                Log.w(TAG, "regressing position by ${posForRender - it}ms from $posForRender to $it!")
        }
        if (spForRender == null) {
            requestLayout()
            return
        }
        var animating = false
        val globalPaddingTop = spForRender!!.first[2]
        var heightSoFar = globalPaddingTop
        var heightSoFarUnscaled = globalPaddingTop
        var heightSoFarWithoutTranslated = heightSoFarUnscaled
        var determineTimeUntilNext = false
        var timeUntilNext = 0uL // TODO: remove if useless
        var firstScrollTarget: Int? = null
        var lastScrollTarget: Int? = null
        canvas.save()
        canvas.translate(globalPaddingHorizontal, heightSoFarUnscaled.toFloat())
        val width = width - globalPaddingHorizontal * 2
        spForRender!!.second.forEach {
            var spanEnd = -1
            var spanStartGradient = -1
            var realGradientStart = -1
            var realGradientEnd = -1
            var wordIdx: Int? = null
            var gradientProgress = Float.NEGATIVE_INFINITY
            val firstTs = it.line?.start ?: ULong.MIN_VALUE
            val lastTs = it.line?.end ?: Long.MAX_VALUE.toULong()
            val timeOffsetForUse = min(
                scaleInAnimTime, min(
                    lerp(
                        firstTs.toFloat(), lastTs.toFloat(),
                        0.5f
                    ) - firstTs.toFloat(),
                    max(firstTs.toFloat(), scaleInAnimTime)
                )
            )
            val fadeInStart = max(firstTs.toLong() - timeOffsetForUse.toLong(), 0L).toULong()
            val fadeInEnd = firstTs + timeOffsetForUse.toULong()
            // If end is implicit, it's the start point of next line, so animate smoothly.
            val fadeOutStart = if (it.line?.endIsImplicit == false) lastTs
            else lastTs - timeOffsetForUse.toULong()
            val fadeOutEnd = if (it.line?.endIsImplicit == false)
                lastTs + (timeOffsetForUse * 2).toULong()
            else lastTs + timeOffsetForUse.toULong()
            val highlight = posForRender in fadeInStart..fadeOutEnd
            val scrollTarget = posForRender in fadeInStart..fadeOutStart
            val scaleInProgress = if (it.line == null) 1f else lerpInv(
                fadeInStart.toFloat(), fadeInEnd.toFloat(),
                posForRender.toFloat()
            )
            val scaleOutProgress = if (it.line == null) 1f else lerpInv(
                fadeOutStart.toFloat(),
                fadeOutEnd.toFloat(),
                posForRender.toFloat()
            )
            val hlScaleFactor = if (it.line == null) 1f else {
                // lerp() argument order is swapped because we divide by this factor
                if (scaleOutProgress in 0f..1f)
                    lerp(
                        smallSizeFactor,
                        1f,
                        scaleColorInterpolator.getInterpolation(scaleOutProgress)
                    )
                else if (scaleInProgress in 0f..1f)
                    lerp(
                        1f,
                        smallSizeFactor,
                        scaleColorInterpolator.getInterpolation(scaleInProgress)
                    )
                else if (highlight)
                    smallSizeFactor
                else 1f
            }
            val isRtl = it.layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT
            val alignmentNormal = if (isRtl) it.layout.alignment == Layout.Alignment.ALIGN_OPPOSITE
            else it.layout.alignment == Layout.Alignment.ALIGN_NORMAL
            if ((scaleInProgress >= -.1f && scaleInProgress <= 1f) ||
                (scaleOutProgress >= -.1f && scaleOutProgress <= 1f))
                animating = true
            if (it.line?.isTranslated != true && it.speaker?.isBackground != true) {
                if (determineTimeUntilNext) {
                    determineTimeUntilNext = false
                    timeUntilNext = max(0uL, (it.line?.start ?: 0uL) - posForRender)
                }
                heightSoFarWithoutTranslated = heightSoFarUnscaled
            }
            if (scrollTarget && firstScrollTarget == null) {
                firstScrollTarget = heightSoFarWithoutTranslated
                determineTimeUntilNext = true
            }
            if (posForRender >= fadeInStart && it.line?.isTranslated != true
                && it.speaker?.isBackground != true) {
                lastScrollTarget = heightSoFarUnscaled
                if (firstScrollTarget == null)
                    determineTimeUntilNext = true
            }
            canvas.translate(
                0f,
                it.paddingTop.toFloat() / hlScaleFactor
            )
            heightSoFar += (it.paddingTop.toFloat() / hlScaleFactor).toInt()
            heightSoFarUnscaled += it.paddingTop
            val culled = heightSoFar > scrollY + height || scrollY - paddingTop > heightSoFar +
                    ((it.layout.height.toFloat() + it.paddingBottom) / hlScaleFactor).toInt()
            if (!culled) {
                if (highlight) {
                    canvas.save()
                    canvas.scale(1f / hlScaleFactor, 1f / hlScaleFactor)
                    if (it.theWords != null) {
                        wordIdx = it.theWords.indexOfLast { it.timeRange.first <= posForRender }
                        if (wordIdx == -1) wordIdx = null
                        if (wordIdx != null) {
                            val word = it.theWords[wordIdx]
                            spanEnd = word.charRange.last + 1 // get exclusive end
                            val gradientEndTime = min(
                                lastTs.toFloat(),
                                word.timeRange.last.toFloat()
                            )
                            val gradientStartTime = min(
                                max(
                                    word.timeRange.first.toFloat(),
                                    firstTs.toFloat()
                                ), gradientEndTime - 1f
                            )
                            gradientProgress = lerpInv(
                                gradientStartTime, gradientEndTime,
                                posForRender.toFloat()
                            )
                            if (gradientProgress in 0f..1f) {
                                spanStartGradient = word.charRange.first
                                // be greedy and eat as much as the line as can be eaten (text that is
                                // same line + is in same text direction). improves font rendering for
                                // japanese if font rendering renders whole text in one pass
                                val wordStartLine = it.layout.getLineForOffset(word.charRange.first)
                                val wordEndLine =
                                    it.layout.getLineForOffset(word.charRange.last)
                                val firstCharOnStartLine = it.layout.getLineStart(wordStartLine)
                                val lastCharOnEndLineExcl = it.layout.getLineEnd(wordEndLine)
                                realGradientStart = it.theWords.lastOrNull {
                                    it.charRange.first >= firstCharOnStartLine && it.charRange.last <
                                            word.charRange.first && it.isRtl != word.isRtl
                                }?.charRange?.last?.plus(1) ?: firstCharOnStartLine
                                realGradientEnd = it.theWords.firstOrNull {
                                    it.charRange.first > word.charRange.last && it.charRange.last <
                                            lastCharOnEndLineExcl && it.isRtl != word.isRtl
                                }?.charRange?.first ?: lastCharOnEndLineExcl
                            }
                        }
                    } else {
                        spanEnd = it.text.length
                    }
                }
                if (!alignmentNormal) {
                    if (!highlight)
                        canvas.save()
                    if (it.layout.alignment != Layout.Alignment.ALIGN_CENTER)
                        canvas.translate(width * (1 - smallSizeFactor / hlScaleFactor), 0f)
                    else // Layout.Alignment.ALIGN_CENTER
                        canvas.translate(width * ((1 - smallSizeFactor / hlScaleFactor) / 2), 0f)
                }
                if (gradientProgress >= -.1f && gradientProgress <= 1f)
                    animating = true
            }
            val spanEndWithoutGradient = if (realGradientStart == -1) spanEnd else realGradientStart
            val inColorAnim = (scaleInProgress in 0f..1f && gradientProgress ==
                    Float.NEGATIVE_INFINITY) || scaleOutProgress in 0f..1f
            var colorSpan = it.text.getSpans<MyForegroundColorSpan>().firstOrNull()
            val cachedEnd = colorSpan?.let { j -> it.text.getSpanEnd(j) } ?: -1
            val wordActiveSpanForLine = when (it.speaker) {
                SpeakerEntity.Male -> wordActiveSpanM.value
                SpeakerEntity.Female -> wordActiveSpanF.value
                SpeakerEntity.Duet -> wordActiveSpanD.value
                else -> wordActiveSpan
            }
            val col = if (!culled) {
                val defaultColorForLine = when (it.speaker) {
                    SpeakerEntity.Male -> defaultTextColorM
                    SpeakerEntity.Female -> defaultTextColorF
                    SpeakerEntity.Duet -> defaultTextColorD
                    else -> defaultTextColor
                }
                val highlightColorForLine = when (it.speaker) {
                    SpeakerEntity.Male -> highlightTextColorM
                    SpeakerEntity.Female -> highlightTextColorF
                    SpeakerEntity.Duet -> highlightTextColorD
                    else -> highlightTextColor
                }
                if (inColorAnim) ColorUtils.blendARGB(
                    if (scaleOutProgress in 0f..1f) highlightColorForLine else
                        defaultColorForLine,
                    if (scaleInProgress in 0f..1f && gradientProgress == Float
                            .NEGATIVE_INFINITY) highlightColorForLine
                    else defaultColorForLine,
                    scaleColorInterpolator.getInterpolation(
                        if (scaleOutProgress in 0f..1f
                        ) scaleOutProgress else scaleInProgress
                    )
                ) else Color.GREEN
            } else Color.RED
            if (cachedEnd != spanEndWithoutGradient || inColorAnim != (colorSpan != wordActiveSpanForLine)) {
                if (cachedEnd != -1) {
                    it.text.removeSpan(colorSpan!!)
                    if (colorSpan != wordActiveSpanForLine && (!inColorAnim || spanEndWithoutGradient == -1)) {
                        if (colorSpanPool.size < 10)
                            colorSpanPool.add(colorSpan)
                        colorSpan = null
                    } else if (inColorAnim && colorSpan == wordActiveSpanForLine)
                        colorSpan = null
                }
                if (spanEndWithoutGradient != -1) {
                    if (inColorAnim && colorSpan == null)
                        colorSpan = colorSpanPool.removeFirstOrNull()
                            ?: @SuppressLint("DrawAllocation") MyForegroundColorSpan(col)
                    else if (!inColorAnim)
                        colorSpan = wordActiveSpanForLine
                    it.text.setSpan(
                        colorSpan, 0, spanEndWithoutGradient,
                        Spanned.SPAN_INCLUSIVE_INCLUSIVE
                    )
                }
            }
            if (inColorAnim && spanEndWithoutGradient != -1) {
                if (colorSpan!! == wordActiveSpanForLine)
                    throw IllegalStateException("colorSpan == wordActiveSpan")
                colorSpan.color = col
            }
            var gradientSpan = it.text.getSpans<MyGradientSpan>().firstOrNull()
            val gradientSpanStart = gradientSpan?.let { j -> it.text.getSpanStart(j) } ?: -1
            val gradientSpanEnd = gradientSpan?.let { j -> it.text.getSpanEnd(j) } ?: -1
            if (gradientSpanStart != realGradientStart || gradientSpanEnd != realGradientEnd) {
                val gradientSpanPoolForLine = when (it.speaker) {
                    SpeakerEntity.Male -> gradientSpanPoolM.value
                    SpeakerEntity.Female -> gradientSpanPoolF.value
                    SpeakerEntity.Duet -> gradientSpanPoolD.value
                    else -> gradientSpanPool
                }
                if (gradientSpanStart != -1) {
                    it.text.removeSpan(gradientSpan!!)
                    if (realGradientStart == -1) {
                        if (gradientSpanPoolForLine.size < 10)
                            gradientSpanPoolForLine.add(gradientSpan)
                        gradientSpan = null
                    }
                }
                if (realGradientStart != -1) {
                    if (gradientSpan == null)
                        gradientSpan = gradientSpanPoolForLine.removeFirstOrNull() ?:
                            when (it.speaker) {
                                SpeakerEntity.Male -> makeGradientSpanM()
                                SpeakerEntity.Female -> makeGradientSpanF()
                                SpeakerEntity.Duet -> makeGradientSpanD()
                                else -> makeGradientSpan()
                            }
                    it.text.setSpan(
                        gradientSpan, realGradientStart, realGradientEnd,
                        Spanned.SPAN_INCLUSIVE_INCLUSIVE
                    )
                }
            }
            if (!culled) {
                if (gradientSpan != null) {
                    gradientSpan.runCount = 0
                    gradientSpan.lastLineCount = -1
                    gradientSpan.lineOffsets = it.words!![wordIdx!!]
                    gradientSpan.totalCharsForProgress = spanEnd - spanStartGradient
                    // We get called once per run + one additional time per run if run direction isn't
                    // same as paragraph direction.
                    gradientSpan.runToLineMappings = it.rlm!!
                    gradientSpan.progress = gradientProgress
                }
                it.layout.draw(canvas)
                if (highlight || !alignmentNormal)
                    canvas.restore()
            }
            canvas.translate(
                0f,
                (it.layout.height.toFloat() + it.paddingBottom) / hlScaleFactor
            )
            heightSoFarUnscaled += it.layout.height + it.paddingBottom
            heightSoFar += ((it.layout.height.toFloat() + it.paddingBottom) / hlScaleFactor).toInt()
        }
        //heightSoFar += globalPaddingBottom
        canvas.restore()
        if (animating)
            invalidate()
        if (isUserInteractingWithScrollView) {
            handler.removeCallbacks(invalidateCallback)
            handler.postDelayed(invalidateCallback, 5000)
            isCallbackQueued = true
            if (spForRender!!.first[3] == 1)
                currentScrollTarget = null
        } else if (!isCallbackQueued && !isScrolling) {
            val scrollTarget = max(0, (firstScrollTarget ?: lastScrollTarget ?: 0) - height / 6)
            if (scrollTarget != currentScrollTarget) {
                smoothScrollTo(
                    0, scrollTarget,
                    lyricAnimTime.toInt()
                )
                currentScrollTarget = scrollTarget
            }
        }
    }

    override fun onTouchEventForChild(event: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    override fun onMeasureForChild(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val myWidth = getDefaultSize(minimumWidth, widthMeasureSpec)
        if (spForMeasure == null || spForMeasure!!.first[0] != myWidth)
            spForMeasure = buildSpForMeasure(lyrics, myWidth)
        setChildMeasuredDimension(
            myWidth,
            getDefaultSize(spForMeasure!!.first[1], heightMeasureSpec)
        )
    }

    override fun onLayoutForChild(left: Int, top: Int, right: Int, bottom: Int) {
        if (spForMeasure == null || spForMeasure!!.first[0] != right - left
            || spForMeasure!!.first[1] != bottom - top)
            spForMeasure = buildSpForMeasure(lyrics, right - left)
        spForRender = spForMeasure!!
        invalidate()
    }

    fun buildSpForMeasure(lyrics: SemanticLyrics?, width: Int): Pair<IntArray, List<SbItem>> {
        val lines = lyrics?.unsyncedText ?: listOf(context.getString(R.string.no_lyric_found) to null)
        val syncedLines = (lyrics as? SemanticLyrics.SyncedLyrics?)?.text
        var lastNonTranslated: SemanticLyrics.LyricLine? = null
        val spLines = lines.mapIndexed { i, it ->
            val syncedLine = syncedLines?.get(i)
            if (syncedLine?.isTranslated != true)
                lastNonTranslated = syncedLine
            val words = syncedLine?.words ?:
            if (prefs.getBooleanStrict("translation_auto_word", false) &&
                syncedLine?.isTranslated == true && lastNonTranslated?.words != null)
                listOf(SemanticLyrics.Word(lastNonTranslated.timeRange, 0..<syncedLine.text.length,
                    findBidirectionalBarriers(syncedLine.text).firstOrNull()?.second == true
                )) else null
            val sb = SpannableStringBuilder(it.first)
            val speaker = syncedLine?.speaker ?: it.second
            val align = if (prefs.getBooleanStrict("lyric_center", false) || speaker?.isGroup == true)
                Layout.Alignment.ALIGN_CENTER
            else if (speaker?.isVoice2 == true)
                Layout.Alignment.ALIGN_OPPOSITE
            else Layout.Alignment.ALIGN_NORMAL
            val tl = syncedLine?.isTranslated == true
            val bg = speaker?.isBackground == true
            val paddingTop = if (tl) 2 else 18
            val paddingBottom = if (i + 1 < (syncedLines?.size ?: -1) &&
                syncedLines?.get(i + 1)?.isTranslated == true
            ) 2 else 18
            val layout = StaticLayoutBuilderCompat.obtain(
                sb, when {
                    speaker == SpeakerEntity.Male && tl -> translationTextPaintM.value
                    speaker == SpeakerEntity.Male -> defaultTextPaintM.value
                    speaker == SpeakerEntity.Female && tl -> translationTextPaintF.value
                    speaker == SpeakerEntity.Female -> defaultTextPaintF.value
                    speaker == SpeakerEntity.Duet && tl -> translationTextPaintD.value
                    speaker == SpeakerEntity.Duet -> defaultTextPaintD.value
                    tl && bg -> translationBackgroundTextPaint
                    tl || bg -> translationTextPaint
                    else -> defaultTextPaint
                }, (width * smallSizeFactor).toInt() - globalPaddingHorizontal.toInt() * 2
            ).setAlignment(align).build()
            val paragraphRtl = layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT
            val alignmentNormal = if (paragraphRtl) align == Layout.Alignment.ALIGN_OPPOSITE
            else align == Layout.Alignment.ALIGN_NORMAL
            var l: StaticLayout? = null
            val lineOffsets = words?.map {
                val ia = mutableListOf<Int>()
                val firstLine = layout.getLineForOffset(it.charRange.first)
                val lastLine = layout.getLineForOffset(it.charRange.last + 1)
                for (line in firstLine..lastLine) {
                    val lineStart = layout.getLineStart(line)
                    var lineEnd = layout.getLineEnd(line)
                    while (lineStart + 1 < lineEnd && (layout.text[lineEnd - 1] == '\n' || layout.text[lineEnd - 1] == '\r'))
                        lineEnd--
                    val firstInLine = max(it.charRange.first, lineStart)
                    val lastInLineExcl = min(it.charRange.last + 1, lineEnd)
                    val horizontalStart = if (paragraphRtl == it.isRtl)
                        layout.getPrimaryHorizontal(firstInLine)
                    else layout.getSecondaryHorizontal(firstInLine)
                    // Recycle the layout if we have multiple words in one line.
                    if (l == null || l.getLineStart(0) != lineStart
                        || (l.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT) != it.isRtl) {
                        // Use StaticLayout instead of Paint.measureText() for V+ useBoundsForWidth
                        // TODO is this working since moving to getPrimaryHorizontal() again?
                        l = StaticLayoutBuilderCompat
                            .obtain(layout.text, layout.paint, Int.MAX_VALUE)
                            .setAlignment(
                                if (it.isRtl) Layout.Alignment.ALIGN_OPPOSITE
                                else Layout.Alignment.ALIGN_NORMAL
                            )
                            .setIsRtl(it.isRtl)
                            .setStart(lineStart)
                            .setEnd(lineEnd)
                            .build()
                    }
                    val w = (l.getPrimaryHorizontal(if (it.isRtl) firstInLine else lastInLineExcl)
                            - l.getPrimaryHorizontal(if (it.isRtl) lastInLineExcl else firstInLine)) +
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
                                // just add a few pixels on top if RTL as approximation :D
                                if (it.isRtl) 5 else 0
                            } else 0
                    val horizontalEnd = horizontalStart + w * if (it.isRtl) -1 else 1
                    val horizontalLeft = min(horizontalStart, horizontalEnd)
                    val horizontalRight = max(horizontalStart, horizontalEnd)
                    ia.add(horizontalLeft.toInt()) // offset from left to start of word
                    ia.add((horizontalRight - horizontalLeft).roundToInt()) // width of text in this line
                    ia.add(firstInLine - it.charRange.first)
                    ia.add(lastInLineExcl - it.charRange.first)
                    ia.add(if (it.isRtl) -1 else 1)
                }
                return@map ia
            }
            SbItem(layout, sb, paddingTop.dpToPx(context), paddingBottom.dpToPx(context),
                words, lineOffsets, lineOffsets?.let { _ ->
                    (0..<layout.lineCount).map { line ->
                        findBidirectionalBarriers(layout.text.subSequence(
                            layout.getLineStart(line), layout.getLineEnd(line))).flatMap {
                            if (it.second == alignmentNormal)
                                listOf(line, line)
                            else
                                listOf(line)
                        }
                    }.flatten()
                }, speaker, syncedLine)
        }
        val heights = spLines.map { it.layout.height + it.paddingTop + it.paddingBottom }
        val globalPaddingTop = if (lyrics is SemanticLyrics.SyncedLyrics) measuredHeight / 6 else
            context.resources.getDimensionPixelSize(R.dimen.lyric_top_padding)
        val globalPaddingBottom = if (lyrics is SemanticLyrics.SyncedLyrics)
                (measuredHeight * (1f - 1f/6f)).toInt() - (heights.lastOrNull() ?: 0) - globalPaddingTop
        else if (lyrics != null) context.resources.getDimensionPixelSize(R.dimen.lyric_bottom_padding) else 0
        return Pair(
            intArrayOf(
                width,
                (if (heights.isNotEmpty())
                (heights.max() * (1 - (1 / smallSizeFactor)) + heights.sum()).toInt()
                        else 0) + globalPaddingTop + globalPaddingBottom,
                globalPaddingTop,
                if (lyrics is SemanticLyrics.SyncedLyrics) 1 else 0
            ), spLines
        )
    }

    override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
        if (spForRender == null) {
            requestLayout()
            return true
        }
        val y = e.y
        var foundItem: SemanticLyrics.LyricLine? = null
        if (lyrics is SemanticLyrics.SyncedLyrics) {
            var heightSoFar = spForRender!!.first[2]
            spForRender!!.second.forEach {
                val firstTs = it.line!!.start.toFloat()
                val lastTs = it.line.end.toFloat()
                val pos = posForRender.toFloat()
                val timeOffsetForUse = min(scaleInAnimTime, min(lerp(firstTs,
                    lastTs, 0.5f) - firstTs, firstTs))
                val highlight = pos >= firstTs - timeOffsetForUse &&
                        pos <= lastTs + timeOffsetForUse
                val scaleInProgress = lerpInv(
                    firstTs - timeOffsetForUse, firstTs + timeOffsetForUse, pos)
                val scaleOutProgress = lerpInv(
                    lastTs - timeOffsetForUse, lastTs + timeOffsetForUse, pos)
                val hlScaleFactor =
                    // lerp() argument order is swapped because we divide by this factor
                    if (scaleOutProgress in 0f..1f)
                        lerp(
                            smallSizeFactor, 1f,
                            scaleColorInterpolator.getInterpolation(scaleOutProgress)
                        )
                    else if (scaleInProgress in 0f..1f)
                        lerp(
                            1f, smallSizeFactor,
                            scaleColorInterpolator.getInterpolation(scaleInProgress)
                        )
                    else if (highlight)
                        smallSizeFactor
                    else 1f
                val myHeight =
                    (it.paddingTop + it.layout.height + it.paddingBottom) / hlScaleFactor
                if (y >= heightSoFar && y <= heightSoFar + myHeight && it.line.isClickable)
                    foundItem = it.line
                heightSoFar += myHeight.toInt()
            }
        }
        if (foundItem != null) {
            instance.setPlayWhenReady(true)
            instance.seekTo(foundItem.start)
            performClick()
        }
        return true
    }

    override fun onDoubleTap(e: MotionEvent): Boolean {
        return false
    }

    override fun onDoubleTapEvent(e: MotionEvent): Boolean {
        return false
    }

    override fun onDown(e: MotionEvent): Boolean {
        return true
    }

    override fun onShowPress(e: MotionEvent) {
        // do nothing
    }

    override fun onSingleTapUp(e: MotionEvent): Boolean {
        return false
    }

    override fun onScroll(
        e1: MotionEvent?,
        e2: MotionEvent,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        return false // handled by parent
    }

    override fun onLongPress(e: MotionEvent) {
        // do nothing
    }

    override fun onFling(
        e1: MotionEvent?,
        e2: MotionEvent,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        return false // handled by parent
    }

    data class SbItem(
        val layout: StaticLayout, val text: SpannableStringBuilder,
        val paddingTop: Int, val paddingBottom: Int, val theWords: List<SemanticLyrics.Word>?,
        val words: List<List<Int>>?, val rlm: List<Int>?, val speaker: SpeakerEntity?,
        val line: SemanticLyrics.LyricLine?
    )

}